package com.inspectortime.service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.inspectortime.domain.ZipCodeGeoData;
import com.inspectortime.repository.Repository;

/**
 * An implementation that relies on a local database containing zip code and
 * latidude/longitude information. Does not rely on outside web service calls.
 * 
 * @author mKvalsvik
 * 
 */
public class LocalDbZipCodeSearchServiceImpl extends AbstractZipCodeSearchService {

	protected static final String FIND_NEARBY_POSTAL_CODES_WEB_SERVICE_URL = "http://ws.geonames.org/findNearbyPostalCodes";
	protected static final float KM_PER_US_MILE = 1.609f;
	public static final double INVALID_ZIP_DISTANCE = 9999.0;

	/**
	 * Note for longitude this is a crude approximation (since the distance
	 * between longitudes becomes shorter when you move away from the equator)
	 * but since this service filters the results anyway it should work ok.
	 * 
	 * @see http://en.wikipedia.org/wiki/Longitude#Degree_length
	 */
	protected static final double MILES_PER_ARC_DEGREE = 69;

	private static Log log = LogFactory
			.getLog(LocalDbZipCodeSearchServiceImpl.class);

	private Repository repository;

	public void setRepository(Repository repository) {
		this.repository = repository;
	}

	public LocalDbZipCodeSearchServiceImpl() {
	}
	
	public LocalDbZipCodeSearchServiceImpl(Repository repository) {
		this.repository = repository;
	}

	public double findDistanceInMiles(String zipCodeA, String zipCodeB) {
		ZipCodeInfo zipA = this.lookupZipCode(zipCodeA);
		if (zipA == null) {
			// TODO: Consider introducing a checked exception instead
			return INVALID_ZIP_DISTANCE;
		}
		ZipCodeInfo zipB = this.lookupZipCode(zipCodeB);
		if (zipB == null) {
			// TODO: Consider introducing a checked exception instead
			return INVALID_ZIP_DISTANCE;
		}
		return zipA.distanceTo(zipB);
	}

	public List<ZipCodeInfo> findNearbyZipCodes(String zipCode,
			int radiusInMiles) {

		ZipCodeGeoData centerZipCodeData = repository.findZipCodeGeoData(zipCode);
		if (centerZipCodeData == null) {
			return Collections.EMPTY_LIST;
		}

		// Compute a crude "square" around the center location
		double latitudeRange = radiusInMiles / MILES_PER_ARC_DEGREE;
		double lowLatitude = centerZipCodeData.getLatitude() - latitudeRange;
		double highLatitude = centerZipCodeData.getLatitude() + latitudeRange;
		double longitudeRange = radiusInMiles / MILES_PER_ARC_DEGREE;
		double lowLongitude = centerZipCodeData.getLongitude() - longitudeRange;
		double highLongitude = centerZipCodeData.getLongitude() + longitudeRange;

		// Obtain list of zip codes within the square from database
		List<ZipCodeGeoData> zipCodesInSquare = repository
				.findZipCodeGeoDataInRange(lowLatitude, highLatitude,
						lowLongitude, highLongitude);
		log.debug("Found " + zipCodesInSquare.size() + " possibly in "
				+ radiusInMiles + "-mile range (square)");

		// Filter out those zip codes that don't fall in the desired radius (circle)
		// Also convert them to ZipCodeInfo objects
		List<ZipCodeInfo> zipCodesInRadius = new ArrayList<ZipCodeInfo>();
		for (ZipCodeGeoData data : zipCodesInSquare) {
			if (data.distanceTo(centerZipCodeData) < radiusInMiles) {
				zipCodesInRadius.add(new ZipCodeInfo(data));
			}
		}
		log.debug("Found " + zipCodesInRadius.size() + " definitely in "
				+ radiusInMiles + "-mile range (circle)");
		return zipCodesInRadius;
	}

	public ZipCodeInfo lookupZipCode(String zipCode) {
		ZipCodeGeoData data = repository.findZipCodeGeoData(zipCode);
		if (data == null) {
			return null;
		}
		return new ZipCodeInfo(data);
	}
	
	
}
